001.Mem0 源码实现与原理解析

Mem 0 是什么,解决什么问题

在工程语境里,Mem 0 可以理解为一层面向 LLM 应用的 长期记忆基础设施。它不直接替代大模型,也不直接替代应用层 agent,而是位于两者之间,专门负责把对话中的“值得长期保留的信息”提炼、存储、检索,并在后续请求中重新提供给上层系统。

它想解决的核心问题不是“怎么让模型看见更多上下文”,而是“怎么让系统只记住真正重要、可持续复用的信息”。如果没有这层 memory pipeline,常见方案通常会落入两个极端:

Mem 0 的工程理念是:不要把长期记忆等同于原始上下文,而是把对话转成可维护的 memory state。 这也是为什么它的源码主线不是 chat,而是 extraction -> update -> storage -> retrieval

1. 项目整体架构概览

1.1 Mem 0 在 LLM 系统中的位置

从代码视角看,Mem 0 不是一个“大模型应用”,而是一层插在应用逻辑与底层存储之间的 memory orchestration layer。它位于:

应用/Agent -> Mem0 SDK -> LLM/Embedder/Vector Store/(可选) Graph Store -> 应用组装 Prompt

在仓库里,这个位置体现得非常明确:

这说明 Mem 0 的核心职责不是替你完成对话,而是负责两件事:

  1. 把对话写成可检索、可更新的 memory
  2. 在查询时把相关 memory 取回来,供上层应用决定如何使用

1.2 与传统 RAG / memory buffer 的区别

从实现上看,Mem 0 与传统 RAG、memory buffer 的关键差异不在“检索”本身,而在 写入时就先做记忆压缩与结构化

和传统 RAG 相比:

和 memory buffer 相比:

为什么这样设计:

1.3 高层组件关系图

可以把 Mem 0 的高层结构理解为以下文字关系图:

SDK/API 调用
-> Memory / AsyncMemory
-> factory 组装依赖
-> LLM 做 memory extraction 与 update decision
-> Embedder 生成向量
-> VectorStore 持久化与检索
-> SQLiteManager 记录变更历史
-> (可选) MemoryGraph 维护实体关系图
-> 上层应用或 proxy 将结果注入 prompt

这里最重要的工程思想是:编排层、模型层、存储层、注入层是解耦的Memory 负责 orchestration,provider 细节交给 factory 和适配器。

Pasted image 20260402155406.png

2. 仓库目录结构解析

2.1 顶层目录职责

按照工程实现的重要性,可以把仓库顶层目录分成三层。

第一层,核心实现:

第二层,产品化与生态集成:

第三层,外围支撑材料:

2.2 mem0/ 包内部结构

mem0/ 目录内部可以继续拆成几个关键子系统:

3. 系统启动与主调用链

3.1 Python SDK 的启动入口

对 Python 用户来说,入口非常简单:

Memory.__init__() 会完成依赖装配:

这一步决定了 Mem 0 的核心设计:Memory 自己不实现模型和存储,而是做统一编排器。这样才能支持 OpenAI、Ollama、Qdrant、PGVector、Neo 4 j 等多种 provider。

3.2 API / Server 调用入口

如果从 REST API 进入,则调用链是:

server/main.py:add_memory()
-> MEMORY_INSTANCE.add(...)
-> Memory.add(...)

检索同理:

server/main.py:search_memories()
-> MEMORY_INSTANCE.search(...)
-> Memory.search(...)

因此 server/ 只是 transport layer,不承载核心逻辑。真正的业务都在 Memory 内部。

3.3 写入主链路:add()

一次标准写入的函数级调用链可以概括为:

Memory.add()
-> _build_filters_and_metadata()
-> 输入归一化(str/dict/list
-> parse_vision_messages()
-> 并发执行 _add_to_vector_store()_add_to_graph()

其中 _add_to_vector_store() 又分成两条路径。

第一条,原文直存路径:

infer=False
-> 对每条 message 直接 embedding_model.embed(...)
-> _create_memory()
-> vector_store.insert(...)
-> SQLiteManager.add_history(..., "ADD")

第二条,默认的“推断式 memory”路径:

infer=True
-> parse_messages()
-> _should_use_agent_memory_extraction()
-> get_fact_retrieval_messages()
-> llm.generate_response(...json...)
-> remove_code_blocks() / extract_json()
-> 得到 facts
-> 对每个 fact 做相似记忆召回
-> get_update_memory_messages(...)
-> 第二次 llm.generate_response(...json...)
-> 执行 ADD / UPDATE / DELETE / NONE

为什么要拆成两段 LLM:

这比分别靠规则做 extraction 和 dedup 更稳,因为真实对话里常见同义改写、信息补全、轻微冲突和时态变化。

Pasted image 20260402155551.png

3.4 检索主链路:search()

检索路径更短,也更像传统检索系统:

Memory.search()
-> _build_filters_and_metadata()
-> 处理 advanced metadata filters
-> 并发执行 _search_vector_store()graph.search()
-> (可选) reranker.rerank(...)
-> 返回 {"results": ..., "relations": ...}

向量检索内部链路是:

Memory._search_vector_store()
-> embedding_model.embed(query, "search")
-> vector_store.search(...)
-> 组装 MemoryItem
-> 应用 threshold

这说明 Mem 0 的检索阶段刻意保持轻量:尽量把复杂性放在写入时,检索时只做 embedding + ANN + optional rerank

Pasted image 20260402155637.png

3.5 读取单条 memory:get()

Memory.get() 的路径非常直接:

Memory.get(memory_id)
-> vector_store.get(vector_id=memory_id)
-> payload 转成 MemoryItem

它不做 LLM 推理,也不查图数据库。原因很简单:按 ID 获取是存储层操作,不应该再经过语义层。

4. Mem 0 的核心数据流

4.1 一次对话输入后的阶段划分

从数据流角度,一次对话写入可以拆成六个阶段。

第一阶段,输入归一化与作用域绑定:

为什么先做这一步:

第二阶段,memory extraction:

为什么使用 fact extraction:

第三阶段,embedding / storage 准备:

这里有一个非常重要的设计点:新事实在真正入库前,会先走一次“用自己检索自己”的流程。这不是多余,而是 update / dedup 的前置步骤。

第四阶段,retrieval / ranking for update:

为什么不用规则去重:

第五阶段,持久化:

写入的真实落点有两个:

为什么双存储:

第六阶段,检索后如何注入 prompt:

严格来说,Memory.search() 只返回结构化结果,不负责 prompt 注入。仓库里内置的自动回填实现位于 mem0/proxy/main.py

Completions.create()
-> _fetch_relevant_memories()
-> _format_query_with_memories()
-> litellm.completion(...)

为什么把 prompt 注入放在 proxy 层而不是 Memory.search()

5. 关键模块源码解析

5.1 Memory manager / store

这是整个系统的控制中心,核心类是:

输入输出结构:

关键职责:

核心策略:

为什么这样设计:

5.2 Memory extraction pipeline

这个模块的关键函数分布在:

输入输出结构:

关键逻辑:

核心算法或策略:

为什么这样设计:

5.3 Retrieval & scoring 逻辑

向量检索相关的关键位置是:

输入输出结构:

关键流程:

  1. embedding_model.embed(query, "search")
  2. vector_store.search(query, vectors, limit, filters)
  3. 将底层命中结果转成统一的 MemoryItem
  4. 如果配置了 reranker,再执行 reranker.rerank(query, original_memories, limit)

评分逻辑的本质:

图检索路径则在 mem0/memory/graph_memory.py:search()

为什么这样设计:

5.4 Memory 更新与去重策略

这是 Mem 0 最有代表性的部分,也是分享时最值得展开的地方。

关键函数:

输入输出结构:

核心策略不是简单的字符串去重,而是四段式管线:

  1. 抽取新事实
  2. 用每个新事实去向量库召回相似旧记忆
  3. 对候选旧记忆去重并做 UUID 临时映射
  4. 让 LLM 决定 ADD / UPDATE / DELETE / NONE

其中有几个很有代表性的工程细节:

为什么这样设计:

这也是 Mem 0 相比“向量库加几条规则”的本质升级点。

5.5 Graph memory 作为扩展模块

图增强路径的默认关键类是 mem0/memory/graph_memory.py:MemoryGraph。这条路径使用 Neo4jGraph 作为默认图访问接口;如果切换到 Neptune 等后端,则对应实现会落在 mem0/graphs/

输入输出结构:

关键流程:

写入:

MemoryGraph.add()
-> _retrieve_nodes_from_data()
-> _establish_nodes_relations_from_data()
-> _search_graph_db()
-> _get_delete_entities_from_search_output()
-> _delete_entities()
-> _add_entities()

检索:

MemoryGraph.search()
-> _retrieve_nodes_from_data(query, filters)
-> _search_graph_db(...)
-> BM25Okapi 重排关系三元组

核心算法或策略:

为什么这样设计:

6. Mem 0 与 Mem 0^g 的区别

从原论文中的命名上看,Mem 0^g 是图增强版;从开源实现上来看,它并不是一个完全独立的新系统,而是 同一个 Memory 主干加上 graph_store 配置后的运行模式

在代码层面的差异主要有三点。

第一,写入路径不同:

第二,检索结果结构不同:

第三,推理目标不同:

但必须强调一个源码事实:

为什么这样做:

7. 设计权衡与工程决策分析

7.1 为什么选择当前架构

Mem 0 当前架构的核心思想可以概括为一句话:

把“理解对话、整理事实、维护记忆”前置到写入时完成,把检索时的在线开销压缩到最小。

这体现在:

这样设计的好处是:

代价也很明确:

7.2 与传统 RAG / vector memory 的 trade-off

相对于传统 RAG:

相对于纯 vector memory:

相对于 conversation buffer:

7.3 哪些地方体现了论文思想,哪些是工程妥协

体现论文思想的地方:

明显的工程妥协也很清楚:

这些妥协背后的原因并不负面,恰恰说明 Mem 0 的目标是 可部署、可替换、可扩展,而不是追求理论上最优的单体架构。

8. 一次完整请求的端到端流程复盘

场景:应用收到一轮新对话,想把它写入 memory,并在下一轮回答前检索相关记忆。

Pasted image 20260402155827.png

8.1 写入阶段

  1. 应用调用 Memory.add(messages, user_id=...),或者通过 server/main.py/memories API 间接调用。
  2. Memory.add() 先用 _build_filters_and_metadata() 生成作用域 metadata,保证后续操作都限定在当前用户/agent/run 下。
  3. 输入消息被归一化;如果包含图像,parse_vision_messages() 先把图像转成文字描述。
  4. Memory.add() 并发启动两条支路:向量记忆支路 _add_to_vector_store(),以及可选图记忆支路 _add_to_graph()
  5. 在向量支路里,parse_messages() 把对话展平;get_fact_retrieval_messages() 选择 prompt;llm.generate_response() 输出 facts
  6. 每个 fact 会先被 embedding,然后去 vector_store.search() 中召回 top-k 旧记忆候选。
  7. 系统把这些候选去重后交给第二个 LLM prompt,让模型输出 action plan:每条新事实应该 ADDUPDATEDELETE 还是 NONE
  8. Memory 根据 action plan 调用 _create_memory()_update_memory()_delete_memory(),把当前状态写入向量库,同时通过 SQLiteManager.add_history() 记录变更历史。
  9. 如果启用了图记忆,MemoryGraph.add() 同步完成实体提取、关系建立、冲突删除与节点/边合并。

8.2 检索阶段

  1. 当应用要回答用户新问题时,调用 Memory.search(query, user_id=...)
  2. Memory.search() 生成 query embedding,并通过 _search_vector_store() 从向量库召回最相关的 memory。
  3. 如果配置了 reranker,调用 reranker.rerank(...) 做二次排序。
  4. 如果启用了图记忆,graph.search() 并发返回实体关系三元组,作为 relations 附加结果。

8.3 Prompt 注入阶段

  1. 如果应用自己掌控 prompt,它可以直接把 search()["results"] 拼进 system prompt;如果使用 proxy,这一步则由包装层自动完成。
  2. 如果使用 mem0/proxy/main.pyCompletions.create(),则会自动:
    1. 后台启动 _async_add_to_memory()
    2. 同步执行 _fetch_relevant_memories()
    3. 通过 _format_query_with_memories() 把 memory 拼到用户问题前
    4. 最后调用 litellm.completion(...)
      Pasted image 20260402155906.png

它强调的是职责边界:Memory 负责提供 memory,应用层或 proxy 负责决定如何消费 memory。

相关链接